package com.xx.doublexchat.common.websocket.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.xx.doublexchat.common.user.dao.UserDao;
import com.xx.doublexchat.common.user.domain.entity.User;
import com.xx.doublexchat.common.user.domain.enums.RoleEnum;
import com.xx.doublexchat.common.user.event.UserOnlineEvent;
import com.xx.doublexchat.common.user.service.ILoginService;
import com.xx.doublexchat.common.user.service.IRoleService;
import com.xx.doublexchat.common.websocket.domain.vo.resp.WSBaseResponse;
import com.xx.doublexchat.common.websocket.service.WebSocketService;
import com.xx.doublexchat.common.websocket.service.adapter.WebSocketAdapter;
import com.xx.doublexchat.common.websocket.utils.NettyUtils;
import com.xx.doublexchat.common.weixin.dto.WSChannelExtraDTO;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.WxMpSemanticQuery;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * WebSocket实现类---处理WebSocket相关逻辑
 *
 * @author 牛顿莱布尼兹欧拉泰勒漩
 * @version 1.0
 * @date 13:50
 */
@Service
@Slf4j
public class WebSocketServiceImpl implements WebSocketService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private ILoginService loginService;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private IRoleService roleService;

    @Qualifier(WS_EXECUTOR)
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    // 管理所有用户的连接(登录/游客)
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MAP = new ConcurrentHashMap<>();
    // 过期时间
    public static final Duration DURATION = Duration.ofHours(1);
    // 最大值
    public static final int MAXIMUM_SIZE = 1000;
    //临时保存登录Code和channel的映射关系
    private static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
            .maximumSize(MAXIMUM_SIZE).expireAfterWrite(DURATION).build();

    /**
     * 所有在线的用户和对应的socket
     */
    private static final ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> ONLINE_UID_MAP = new ConcurrentHashMap<>();

    /**
     * websocket通信线程池
     */
    public static final String WS_EXECUTOR = "websocketExecutor";

    /**
     * 保存连接
     *
     * @author 牛顿莱布尼兹欧拉泰勒漩
     * @version 1.0
     * @date 13:56
     */
    @Override
    public void connect(Channel channel) {
        ONLINE_WS_MAP.put(channel, new WSChannelExtraDTO());
    }

    /**
     * 处理WebSocket登录请求
     *
     * @param channel     webSocket通道
     * @param wxMpService
     */
    @SneakyThrows
    @Override
    public void handleLoginReq(Channel channel, WxMpService wxMpService) {
        // 生成场景值ID
        Integer sceneId = generateLonginCode(channel);
        System.out.println("发送给给微信的场景值ID:---" + sceneId);
        // 找微信申请带参二维码
        WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(sceneId, (int) DURATION.getSeconds());
        // 把二维码推送给前端
        sendMsg(channel, WebSocketAdapter.buildResponse(wxMpQrCodeTicket));
        System.out.println("发送二维码成功");
    }

    /**
     * 主动溢出channel
     *
     * @param channel 通道
     */
    @Override
    public void remove(Channel channel) {
        // 用户下线
        WSChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_MAP.get(channel);
        Optional<Long> uidOptional = Optional.ofNullable(wsChannelExtraDTO)
                .map(WSChannelExtraDTO::getUid);
        boolean offlineAll = offline(channel, uidOptional);
        // 下线成功且uid不为空
        if (offlineAll && uidOptional.isPresent()) {
            // TODO 推送消息用户下线

        }
    }

    /**
     * 用户下线
     * @param channel 通道
     * @param uidOptional uid
     * @return
     */
    private boolean offline(Channel channel, Optional<Long> uidOptional) {
        //在线websocket MAP 移除当前channel
        ONLINE_WS_MAP.remove(channel);
        // 当uidOptional 存在则需要移除uid对应channel map中的当前channel数据
        if (uidOptional.isPresent()) {
            CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uidOptional.get());
            if (CollectionUtil.isNotEmpty(channels)) {
                channels.removeIf(ch -> Objects.equals(ch, channel));
            }
            return CollectionUtil.isEmpty(ONLINE_UID_MAP.get(uidOptional.get()));
        }
        return true;
    }

    /**
     * 扫码登录成功
     *
     * @param sceneId
     * @param id
     */
    @Override
    public void scanLoginSuccess(Integer sceneId, Long id) {
        // 确认连接存在
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(sceneId);

        if (Objects.isNull(channel)) {
            return;
        }
        User user = userDao.getById(id);
        // 移除sceneId
        WAIT_LOGIN_MAP.invalidate(sceneId);
        // 调用登录模块获取token
        String token = loginService.login(id);
        // 用户登录成功
        loginSuccess(channel, user, token);
    }

    /**
     * 等待授权
     *
     * @param sceneId
     */
    @Override
    public void waitAuthorize(Integer sceneId) {
        // 确认连接存在
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(sceneId);
        if (Objects.isNull(channel)) {
            return;
        }
        sendMsg(channel, WebSocketAdapter.buildResponse());
    }

    /**
     * 发送信息给WebSocket
     *
     * @author 牛顿莱布尼兹欧拉泰勒漩
     * @version 1.0
     * @date 14:19
     */
    private void sendMsg(Channel channel, WSBaseResponse<?> response) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(response)));
    }

    /**
     * 生成随机码
     *
     * @author 牛顿莱布尼兹欧拉泰勒漩
     * @version 1.0
     * @date 14:11
     */
    private Integer generateLonginCode(Channel channel) {
        Integer code;
        do {
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        } while (Objects.nonNull(WAIT_LOGIN_MAP.asMap().putIfAbsent(code, channel)));//将code和channel放入临时Map中
        return code;
    }

    /**
     * 认证
     *
     * @param channel
     * @param token
     */
    @Override
    public void authorize(Channel channel, String token) {
        Long uid = loginService.getValidUid(token);
        if (Objects.nonNull(uid)) {
            User user = userDao.getById(uid);
            loginSuccess(channel, user, token);
        } else {
            sendMsg(channel, WebSocketAdapter.buildInvalidTokenResponse());
        }
    }

    /**
     * 发送消息给所有用户
     *
     * @param msg
     */
    @Override
    public void sendMsgToAll(WSBaseResponse<?> msg) {
        ONLINE_WS_MAP.forEach((channel, ext) -> {
            threadPoolTaskExecutor.execute(() -> {
                sendMsg(channel, msg);
            });
        });
    }

    /**
     * 发送消息给指定用户
     *
     * @param wsBaseMsg 消息主题
     * @param uid       需要发送的uid
     */
    @Override
    public void sendToUid(WSBaseResponse<?> wsBaseMsg, Long uid) {
        //通过uid获取用户对应的channel
        CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uid);
        if (CollectionUtil.isEmpty(channels)) {
            log.info("用户：{}不在线", uid);
            return;
        }
        // 多线程发送消息
        channels.forEach((channel) -> {
            threadPoolTaskExecutor.execute(() -> sendMsg(channel, wsBaseMsg));
        });
    }

    /**
     * 发送消息给所有用户
     *
     * @param wsBaseMsg 消息主体
     * @param skipUid   需要跳过的人
     */
    @Override
    public void sendToAllOnline(WSBaseResponse<?> wsBaseMsg, Long skipUid) {
        ONLINE_WS_MAP.forEach((channel, ext) -> {
            if (Objects.nonNull(skipUid) && Objects.equals(ext.getUid(), skipUid)) {
                return;
            }
            threadPoolTaskExecutor.execute(() -> sendMsg(channel, wsBaseMsg));
        });
    }


    /**
     * 用户上线成功
     *
     * @param channel
     * @param user
     * @param token
     */
    private void loginSuccess(Channel channel, User user, String token) {
        //保存uid对应的channel ---> 方便后续推送消息
        online(user.getId(), channel);
        // 保存channel的对应UID
        WSChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_MAP.get(channel);
        wsChannelExtraDTO.setUid(user.getId());
        // 推送成功信息
        sendMsg(channel, WebSocketAdapter.buildResponse(user, token, roleService.hasPower(user.getId(), RoleEnum.CHAT_MANAGER)));
        // 用户上线成功事件
        user.setLastOptTime(new Date());
        user.refreshIp(NettyUtils.getAttr(channel, NettyUtils.IP));
        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
    }

    /**
     * 用户上线保存uid对应的channel到map中，便于后续进行消息推送
     *
     * @param uid
     * @param channel
     */
    private void online(Long uid, Channel channel) {
        getOrInitChannelExt(channel).setUid(uid);
        ONLINE_UID_MAP.putIfAbsent(uid, new CopyOnWriteArrayList<>());
        ONLINE_UID_MAP.get(uid).add(channel);
        NettyUtils.setAttr(channel, NettyUtils.UID, uid);
    }

    /**
     * 如果在线列表不存在，就先把该channel放进在线列表
     *
     * @param channel
     * @return
     */
    private WSChannelExtraDTO getOrInitChannelExt(Channel channel) {
        //从在线MAP中获取对应链接的dto
        WSChannelExtraDTO wsChannelExtraDTO =
                ONLINE_WS_MAP.getOrDefault(channel, new WSChannelExtraDTO());
        WSChannelExtraDTO old = ONLINE_WS_MAP.putIfAbsent(channel, wsChannelExtraDTO);
        return ObjectUtil.isNull(old) ? wsChannelExtraDTO : old;
    }
}
